package com.wedroid.r2d2.http;

import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.demo2do.core.utils.JsonUtils;
import com.wedroid.r2d2.Constant;
import com.wedroid.r2d2.contact.Contact;
import com.wedroid.r2d2.message.SendingMsg;
import com.wedroid.r2d2.support.BaseRequest;
import com.wedroid.r2d2.support.MediaRequest;
import com.wedroid.r2d2.support.RequestParamBuilder;
import com.wedroid.r2d2.support.ResponseResult;
import com.wedroid.r2d2.support.response.ContactResponse;
import com.wedroid.r2d2.support.response.DroidResponseHandler;
import com.wedroid.r2d2.support.response.GroupResponse;
import com.wedroid.r2d2.support.response.InitResponse;
import com.wedroid.r2d2.support.response.RedirectURIResponse;
import com.wedroid.r2d2.support.response.SendingMsgResponse;
import com.wedroid.r2d2.support.response.StatusNotifyResponse;
import com.wedroid.r2d2.support.response.SyncCheckResponse;
import com.wedroid.r2d2.support.response.SyncResponse;
import com.wedroid.r2d2.support.response.UploadMediaResponse;
import com.wedroid.r2d2.support.response.login.LoginStatus;
import com.wedroid.r2d2.support.response.login.LoginStatusResponse;
import com.wedroid.r2d2.support.response.login.UUIDResponse;
import com.wedroid.r2d2.support.sync.SyncKey;

/**
 * @author lute
 */
@Component("defaultRequestManager")
public class DefaultRequestManager implements RequestManager {

    private static final Logger logger = LoggerFactory.getLogger(DefaultRequestManager.class);

    private String passTicket;

    private BaseRequest baseRequest;

    private String redirectURI;

    private String origin;

    private SyncKey syncKey;

    /** Media upload index */
    private int mediaUploadIdx;

    private Contact user;

    @Autowired
    private DroidHttpClient client;

    /* (non-Javadoc)
     * @see com.wedroid.r2d2.http.RequestManager#shutdown()
     */
    public void shutdown() {
        client.shutdown();
    }

    /* (non-Javadoc)
     * @see com.wedroid.r2d2.http.RequestManager#getUUID()
     */
    public String getUUID() {
        HttpUriRequest request = RequestBuilder.get(ENDPOINT_UUID)
                                               .addParameter("appid", Constant.APPID)
                                               .addParameter("fun", "new")
                                               .addParameter("lang", "en_US")
                                               .addParameter("_", String.valueOf(new Date().getTime()))
                                               .build();

        ResponseHandler<UUIDResponse> responseHandler = new DroidResponseHandler<>(UUIDResponse.class);
        UUIDResponse response = client.execute(request, responseHandler);
        if (response != null && response.isSuccess()) {
            return response.getUuid();
        }
        return null;
    }

    /* (non-Javadoc)
     * @see com.wedroid.r2d2.http.RequestManager#requestLoginStatus(java.lang.String, int)
     */
    public LoginStatus requestLoginStatus(String uuid, int tip) {
        HttpUriRequest request = RequestBuilder.get(ENDPOINT_LOGIN_POLL)
                                               .addParameter("tip", String.valueOf(tip))
                                               .addParameter("uuid", uuid)
                                               .addParameter("_", String.valueOf(new Date().getTime() / 1000L))
                                               .build();
        ResponseHandler<LoginStatusResponse> responseHandler = new DroidResponseHandler<>(LoginStatusResponse.class);
        LoginStatusResponse response = client.execute(request, responseHandler);
        if (response != null) {
            LoginStatus status = response.getLoginStatus();
            if (response.isSuccess()) {
                this.redirectURI = response.getRedirectURI() + "&fun=new";
                this.origin = response.getOrigin();
            }
            return status;
        }
        return LoginStatus.ERROR;
    }

    /* (non-Javadoc)
     * @see com.demo2do.wxdroid.handler.DroidRequestHandler#postLoginRedirect()
     */
    public boolean postLoginRedirect() {
        HttpUriRequest request = RequestBuilder.get(redirectURI).build();
        ResponseHandler<RedirectURIResponse> responseHandler = new DroidResponseHandler<>(RedirectURIResponse.class);

        RedirectURIResponse response = client.execute(request, responseHandler);
        if (response != null && response.isSuccess()) {
            this.baseRequest = response.getBaseRequest();
            this.passTicket = response.getPassTicket();
            return true;
        } else {
            logger.error("#postLoginRedirect - Parsing redirect URI response may have something wrong...");
            return false;
        }
    }

    /* (non-Javadoc)
     * @see com.wedroid.r2d2.handler.DroidRequestHandler#init()
     */
    public ResponseResult<InitResponse> init() {

        ResponseResult<InitResponse> result = new ResponseResult<>();

        try {
            URI uri = new URIBuilder(origin + PATH_INIT).addParameter("r", String.valueOf(new Date().getTime() / 1000L))
                                                        .addParameter("lang", "en_US")
                                                        .addParameter("pass_ticket", this.passTicket)
                                                        .build();

            String payload = JsonUtils.toJsonString(baseRequest.build());
            StringEntity entity = new StringEntity(payload, ContentType.APPLICATION_JSON);

            HttpUriRequest request = RequestBuilder.post(uri).setEntity(entity).build();
            ResponseHandler<InitResponse> responseHandler = new DroidResponseHandler<>(InitResponse.class);

            InitResponse response = client.execute(request, responseHandler);
            if (response != null && response.isSuccess()) {
                syncKey = response.getSyncKey();
                user = response.getUser();
            }
            result.response(response);
        } catch (URISyntaxException e) {
            result.fail(e.getMessage());
            logger.error("#init - Building URI encounters syntax exception: {}", e);
        }

        return result;
    }

    /* (non-Javadoc)
     * @see com.demo2do.wxdroid.handler.DroidRequestHandler#statusNotify()
     */
    public void statusNotify() {
        String payload = new RequestParamBuilder(baseRequest.changeUinToInteger().build())
                .addParameter("Code", 3)
                .addParameter("FromUserName", user.getUserName())
                .addParameter("ToUserName", user.getUserName())
                .addParameter("ClientMsgId", new Date().getTime())
                .buildJSONString();
        StringEntity entity = new StringEntity(payload, ContentType.APPLICATION_JSON);

        try {
            URI uri = new URIBuilder(origin + PATH_STATUS_NOTIFY).addParameter("lang", "zh_CN")
                                                                 .addParameter("pass_ticket", passTicket)
                                                                 .build();
            HttpUriRequest request = RequestBuilder.post(uri).setEntity(entity).build();

            ResponseHandler<StatusNotifyResponse> responseHandler = new DroidResponseHandler<>(StatusNotifyResponse.class);
            StatusNotifyResponse response = client.execute(request, responseHandler);

            if (response != null && response.isSuccess()) {
                logger.trace("#statusNotify - Status notify request returns success.");
            }
        } catch (URISyntaxException e) {
            logger.error("#statusNotify - Building URI encounters syntax exception: {}", e);
        }
    }

    /* (non-Javadoc)
     * @see com.wedroid.r2d2.handler.DroidRequestHandler#syncCheck()
     */
    public ResponseResult<SyncCheckResponse> syncCheck() {

        ResponseResult<SyncCheckResponse> result = new ResponseResult<>();

        List<NameValuePair> pairs = new ArrayList<NameValuePair>();
        pairs.add(new BasicNameValuePair("r", String.valueOf(new Date().getTime())));
        pairs.add(new BasicNameValuePair("sid", baseRequest.getSid()));
        pairs.add(new BasicNameValuePair("uin", baseRequest.getUin()));
        pairs.add(new BasicNameValuePair("skey", baseRequest.getSkey()));
        pairs.add(new BasicNameValuePair("deviceid", baseRequest.getDeviceId()));
        pairs.add(new BasicNameValuePair("synckey", syncKey.getSyncKeyString()));
        pairs.add(new BasicNameValuePair("_", String.valueOf(new Date().getTime())));
        String queryString = URLEncodedUtils.format(pairs, Consts.UTF_8);

        try {
            String[] prefixes = { "webpush.", "webpush2." };
            for (String prefix : prefixes) {
                URI uri = new URI(origin);

                String url = new StringBuilder(uri.getScheme()).append("://")
                                                               .append(prefix)
                                                               .append(uri.getHost())
                                                               .append(PATH_SYNC_CHECK)
                                                               .append("?")
                                                               .append(queryString)
                                                               .toString();
                HttpUriRequest request = RequestBuilder.get(url).build();
                ResponseHandler<SyncCheckResponse> responseHandler = new DroidResponseHandler<>(SyncCheckResponse.class);

                SyncCheckResponse response = client.execute(request, responseHandler);
                result.response(response);

                // Break the loop in advance
                if (result.isValid()) {
                    return result;
                }
            }
        } catch (URISyntaxException e) {
            result.fail(e.getMessage());
            logger.error("#syncCheck - Sync check request encounters URI syntax exception: {}", e);
        }
        return result;
    }

    /* (non-Javadoc)
     * @see com.demo2do.wxdroid.handler.DroidRequestHandler#sync()
     */
    public ResponseResult<SyncResponse> sync() {

        ResponseResult<SyncResponse> result = new ResponseResult<>();

        String payload = new RequestParamBuilder(baseRequest.build())
                .addParameter("SyncKey", syncKey.getSyncKey())
                .addParameter("rr", new Date().getTime() / 1000)
                .buildJSONString();
        StringEntity entity = new StringEntity(payload, ContentType.APPLICATION_JSON);

        try {
            URI uri = new URIBuilder(origin + PATH_SYNC).addParameter("sid", baseRequest.getSid())
                                                        .addParameter("skey", baseRequest.getSkey())
                                                        .addParameter("lang", "en_US")
                                                        .addParameter("pass_ticket", passTicket)
                                                        .build();
            HttpUriRequest request = RequestBuilder.post(uri).setEntity(entity).build();
            ResponseHandler<SyncResponse> responseHandler = new DroidResponseHandler<>(SyncResponse.class);

            SyncResponse response = client.execute(request, responseHandler);
            result.response(response);

            if (response != null && response.isSuccess()) {
                this.syncKey = response.getSyncKey();
            } else if (response != null && !response.isSuccess()) {
                result.fail(response.getErrorMessage());
            }
        } catch (URISyntaxException e) {
            result.fail(e.getMessage());
            logger.error("#sync - Syncing encounters syntax exception: {}", e);
        }
        return result;
    }

    /* (non-Javadoc)
     * @see com.wedroid.r2d2.handler.DroidRequestHandler#getContacts()
     */
    public ResponseResult<ContactResponse> getContacts() {

        ResponseResult<ContactResponse> result = new ResponseResult<>();

        try {
            URI uri = new URIBuilder(origin + PATH_CONTACT).addParameter("pass_ticket", passTicket)
                                                           .addParameter("skey", baseRequest.getSkey())
                                                           .addParameter("r", String.valueOf(new Date().getTime()))
                                                           .build();
            HttpUriRequest request = RequestBuilder.post(uri).build();
            ResponseHandler<ContactResponse> responseHandler = new DroidResponseHandler<>(ContactResponse.class);

            ContactResponse response = client.execute(request, responseHandler);

            result.response(response);
            if (response != null && !response.isSuccess()) {
                result.fail(response.getErrorMessage());
            }

        } catch (URISyntaxException e) {
            result.fail(e.getMessage());
            logger.error("#getContact - Getting contact encounters URI syntax exception: {}", e);
        }
        return result;
    }

    /* (non-Javadoc)
     * @see com.demo2do.wxdroid.handler.DroidRequestHandler#getGroupMember(java.util.List)
     */
    public ResponseResult<GroupResponse> getGroupMember(List<Contact> groups) {

        ResponseResult<GroupResponse> result = new ResponseResult<>();

        if (groups == null) {
            result.fail("Null groups for members...");
            return result;
        }

        List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
        for (Contact group : groups) {
            Map<String, Object> entry = new HashMap<String, Object>();
            entry.put("UserName", group.getUserName());
            entry.put("EncryChatRoomId", "");
            list.add(entry);
        }

        String payload = new RequestParamBuilder(baseRequest.build())
                                .addParameter("Count", groups.size())
                                .addParameter("List", list)
                                .buildJSONString();
        StringEntity entity = new StringEntity(payload, ContentType.APPLICATION_JSON);

        try {

            URI uri = new URIBuilder(origin + PATH_BATCH_CONTACT).addParameter("type", "ex")
                                                                 .addParameter("r", String.valueOf(new Date().getTime()))
                                                                 .addParameter("pass_ticket", passTicket)
                                                                 .build();
            HttpUriRequest request = RequestBuilder.post(uri).setEntity(entity).build();
            ResponseHandler<GroupResponse> responseHandler = new DroidResponseHandler<>(GroupResponse.class);

            GroupResponse response = client.execute(request, responseHandler);

            result.response(response);
            if (response != null && !response.isSuccess()) {
                result.fail(response.getErrorMessage());
            }

        } catch (URISyntaxException e) {
            result.fail(e.getMessage());
            logger.error("#getGroupMember - Getting group member encounters URI syntax exception: {}", e);
        }
        return result;
    }

    /* (non-Javadoc)
     * @see com.wedroid.r2d2.http.RequestManager#getImageMessage(java.lang.String, boolean)
     */
    public CloseableHttpResponse getImageMessage(String msgId, boolean isSlave) {
        try {
            URIBuilder builder = new URIBuilder(origin + PATH_IMAGE_MESSAGE)
                                        .addParameter("MsgID", msgId)
                                        .addParameter("skey", baseRequest.getSkey());
            if (isSlave) {
                builder.addParameter("type", "slave");
            }

            HttpUriRequest request = RequestBuilder.get(builder.build()).build();
            return client.execute(request);
        } catch (URISyntaxException e) {
            logger.error("#getImageMessage - Getting image message encounters URI syntax exception: {}", e);
        }
        return null;
    }

    /* (non-Javadoc)
     * @see com.wedroid.r2d2.http.RequestManager#getVoiceMessage(java.lang.String)
     */
    public CloseableHttpResponse getVoiceMessage(String msgId) {
        try {
            URI uri = new URIBuilder(origin + PATH_VOICE_MESSAGE).addParameter("msgid", msgId)
                                                                 .addParameter("skey", baseRequest.getSkey())
                                                                 .build();
            HttpUriRequest request = RequestBuilder.get(uri).build();
            return client.execute(request);
        } catch (URISyntaxException e) {
            logger.error("#getVoiceMessage - Getting voice message encounters URI syntax exception: {}", e);
        }
        return null;
    }

    /* (non-Javadoc)
     * @see com.wedroid.r2d2.http.RequestManager#getHeadImg(com.wedroid.r2d2.contact.Contact)
     */
    public CloseableHttpResponse getHeadImg(Contact contact) {
        try {
            URI uri = new URIBuilder(origin + PATH_IMAGE_HEAD)
                            .addParameter("seq", "0")
                            .addParameter("username", contact.getUserName())
                            .addParameter("skey", baseRequest.getSkey())
                            .build();
            HttpUriRequest request = RequestBuilder.get(uri).build();
            return client.execute(request);
        } catch (URISyntaxException e) {
            logger.error("#getHeadImg - Getting head image encounters URISyntaxException: {}", e);
        }
        return null;
    }

    /* (non-Javadoc)
     * @see com.wedroid.r2d2.http.RequestManager#getIcon(com.wedroid.r2d2.contact.Contact, java.lang.String)
     */
    public CloseableHttpResponse getIcon(Contact contact, String chatRoomId) {
        try {
            URIBuilder builder = new URIBuilder(origin + PATH_IMAGE_ICON)
                                        .addParameter("seq", "0")
                                        .addParameter("username", contact.getUserName())
                                        .addParameter("skey", baseRequest.getSkey());
            if (StringUtils.isNotBlank(chatRoomId)) {
                builder.addParameter("chatroomid", chatRoomId);
            }
            HttpUriRequest request = RequestBuilder.get(builder.build()).build();
            return client.execute(request);
        } catch (URISyntaxException e) {
            logger.error("#getIcon - Getting icon encounters URISyntaxException: {}", e);
        }
        return null;
    }

    /* (non-Javadoc)
     * @see com.wedroid.r2d2.http.RequestManager#getPublicLinkImg(java.lang.String)
     */
    public CloseableHttpResponse getPublicLinkImg(String path) {
        HttpUriRequest request = RequestBuilder.get(origin + path).build();
        return client.execute(request);
    }

    /* (non-Javadoc)
     * @see com.wedroid.r2d2.http.RequestManager#sendText(java.lang.String, java.lang.String)
     */
    public ResponseResult<SendingMsgResponse> sendText(String content, String toUserName) {
        SendingMsg sendingMsg = SendingMsg.ofTextMessage(content, user.getUserName(), toUserName);
        return sendMessage(sendingMsg);
    }

    /* (non-Javadoc)
     * @see com.wedroid.r2d2.http.RequestManager#sendImage(java.lang.String, java.lang.String)
     */
    public ResponseResult<SendingMsgResponse> sendImage(String path, String toUserName) {
        // Upload media first
        Optional<String> mediaId = uploadMedia(path, user.getUserName(), toUserName);
        if (mediaId.isPresent()) {
            logger.trace("#sendImage - Get uploading meida id [{}]", mediaId.get());

            SendingMsg sendingMsg = SendingMsg.ofImageMessage(mediaId.get(), user.getUserName(), toUserName);
            return sendMessage(sendingMsg);
        }
        return new ResponseResult<>();
    }

    /**
     * @param sendingMsg
     * @return
     */
    private ResponseResult<SendingMsgResponse> sendMessage(SendingMsg sendingMsg) {
        ResponseResult<SendingMsgResponse> result = new ResponseResult<>();

        String payload = new RequestParamBuilder(baseRequest.build())
                                .addParameter("Msg", sendingMsg)
                                .addParameter("Scene", 0)
                                .buildJSONString();
        StringEntity entity = new StringEntity(payload, ContentType.APPLICATION_JSON);

        String sendingPath = determineMessageSendingPath(sendingMsg);

        try {

            URI uri = new URIBuilder(origin + sendingPath)
                                            .addParameter("pass_ticket", passTicket)
                                            .build();
            HttpUriRequest request = RequestBuilder.post(uri).setEntity(entity).build();
            ResponseHandler<SendingMsgResponse> responseHandler = new DroidResponseHandler<>(SendingMsgResponse.class);

            SendingMsgResponse response = client.execute(request, responseHandler);
            result.response(response);
            if (response != null && !response.isSuccess()) {
                result.fail(response.getErrorMessage());
            }
        } catch (URISyntaxException e) {
            logger.error("#sendMessage - Sending message encounters URISyntaxException: {}", e);
        }
        return result;
    }

    /**
     * @param sendingMsg
     * @return
     */
    private String determineMessageSendingPath(SendingMsg sendingMsg) {
        switch (sendingMsg.getMessageType()) {
        case TEXT:
            return PATH_SEND_TEXT;
        case IMAGE:
            return PATH_SEND_IMAGE;
        case EMOTICON:
            return PATH_SEND_EMOTICON;
        case APP_ATTACH:
            return PATH_SEND_FILE;
        default:
            return PATH_SEND_TEXT;
        }
    }

    /**
     * Upload media file
     *
     * @param pathString
     * @param fromUserName
     * @param toUserName
     * @return
     */
    private Optional<String> uploadMedia(String pathString, String fromUserName, String toUserName) {
        Path path = Paths.get(pathString);
        if (Files.exists(path) && Files.isRegularFile(path) && Files.isReadable(path)) {
            File file = path.toFile();

            // Guess file mimeType
            String mimeType = URLConnection.guessContentTypeFromName(file.getName());
            logger.trace("#uploadMedia - Guessed content type [{}]", mimeType);

            // Determine type from mimeType
            String mediaType = (StringUtils.isNotBlank(mimeType) &&
                    "image".equals(mimeType.split("/")[0])) ? "pic" : "doc";
            logger.trace("#uploadMedia - Get media type [{}]", mediaType);

            // Determine file last modified date
            String lastModifiedDate =
                    DateTimeFormatter.ofPattern("E MMM dd yyyy kk:mm:ss 'GMT'Z (z)")
                                     .format(
                                         ZonedDateTime.ofInstant(
                                             Instant.ofEpochSecond(file.lastModified()),
                                             ZoneId.systemDefault()
                                         )
                                     );
            logger.trace("#uploadMedia - Get file last modified date [{}]", lastModifiedDate);

            // Construct upload request
            MediaRequest mediaRequest = new MediaRequest(this.baseRequest, path, fromUserName, toUserName);
            String uploadmediarequest = JsonUtils.toJsonString(mediaRequest);
            logger.trace("#uploadMedia - Get upload media request [{}]", uploadmediarequest);

            // Get `webwx_data_ticket` from cookie
            Optional<String> optionalTicket = this.client.getCookie("webwx_data_ticket");
            String webwxDataTicket = optionalTicket.isPresent() ? optionalTicket.get() : "";
            logger.trace("#uploadMedia - Get webwx_data_ticket [{}] from cookie", webwxDataTicket);

            // Media upload URL alternations
            List<String> prefixes = Arrays.asList("file.", "file2.");
            for (String prefix : prefixes) {

                HttpEntity httpEntity = MultipartEntityBuilder
                                            .create()
                                            .setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
                                            .addTextBody("id", "WU_FILE_" + this.mediaUploadIdx, ContentType.DEFAULT_BINARY)
                                            .addTextBody("name", file.getName(), ContentType.DEFAULT_BINARY)
                                            .addTextBody("type", StringUtils.isBlank(mimeType) ? "application/octet-stream" : mimeType, ContentType.DEFAULT_BINARY)
                                            .addTextBody("lastModifiedDate", lastModifiedDate, ContentType.DEFAULT_BINARY)
                                            .addTextBody("size", String.valueOf(file.length()), ContentType.DEFAULT_BINARY)
                                            .addTextBody("mediatype", mediaType, ContentType.DEFAULT_BINARY)
                                            .addTextBody("uploadmediarequest", uploadmediarequest, ContentType.DEFAULT_BINARY)
                                            .addTextBody("webwx_data_ticket", webwxDataTicket, ContentType.DEFAULT_BINARY)
                                            .addTextBody("pass_ticket", this.passTicket, ContentType.DEFAULT_BINARY)
                                            .addBinaryBody("filename", file, ContentType.DEFAULT_BINARY, file.getName())
                                            .build();

                try {
                    URI uri = new URI(origin);

                    String url = new StringBuilder(uri.getScheme()).append("://")
                                                                   .append(prefix)
                                                                   .append(uri.getHost())
                                                                   .append(PATH_UPLOAD_MEDIA)
                                                                   .append("?f=json")
                                                                   .toString();

                    HttpUriRequest request = RequestBuilder.post(url)
                                                           .setEntity(httpEntity)
                                                           .build();
                    ResponseHandler<UploadMediaResponse> responseHandler = new DroidResponseHandler<>(UploadMediaResponse.class);
                    UploadMediaResponse response = client.execute(request, responseHandler);
                    if (response.isSuccess()) {
                        return Optional.of(response.getMediaId());
                    }

                } catch (URISyntaxException e) {
                    logger.error("#uploadMedia - Uploading media encounters URISyntaxException: {}", e);
                }
            }
        }
        return Optional.empty();
    }

}
